//
// (C) Copyright 2009 Irantha Suwandarathna (irantha@gmail.com)
// All rights reserved.
//

/* Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


using System;
using System.Threading;
using System.Reflection;
using System.Text;
using System.Collections.Generic;
using EffiProz.Core.Lib;
using EffiProz.Core.Persist;
using EffiProz.Core.RowIO;
using EffiProz.Core.Indexes;
using EffiProz.Core.Errors;
using EffiProz.Core.Rights;
using EffiProz.Core.Navigators;
using EffiProz.Core.DataTypes;
using EffiProz.Core.Results;

namespace EffiProz.Core
{
    // peterhudson@users 20020130 - patch 478657 by peterhudson - triggers support
    // fredt@users 20020130 - patch 1.7.0 by fredt
    // added new class as jdk 1.1 does not allow use of LinkedList
    // fredt@users 20030727 - signature and other alterations
    // fredt@users 20040430 - changes by mattshaw@users to allow termination of the
    // trigger thread -

    /**
     *  Represents an HSQLDB Trigger definition. <p>
     *
     *  Provides services regarding HSLDB Trigger execution and metadata. <p>
     *
     *  Development of the trigger implementation sponsored by Logicscope
     *  Realisations Ltd
     *
     * @author Peter Hudson - Logicscope Realisations Ltd
     * @version  1.7.0 (1.0.0.3)
     *      Revision History: 1.0.0.1 First release _in hsqldb 1.61
     *      1.0.0.2 'nowait' support to prevent deadlock 1.0.0.3 multiple row
     *      queue for each trigger
     */
    public class TriggerDef : SchemaObject
    {
        public const int OLD_ROW = 0;
        public const int NEW_ROW = 1;
        public const int OLD_TABLE = 2;
        public const int NEW_TABLE = 3;

        //
        public const int NUM_TRIGGER_OPS = 3;                      // {ins,del,upd}
        public const int NUM_TRIGS = NUM_TRIGGER_OPS * 3;    // {b}{fer}, {a},{fer, fes}

        //
        public static TriggerDef[] emptyArray = new TriggerDef[] { };
        public Table[] transitions;
        public RangeVariable[] rangeVars;
        public Expression condition;
        bool hasTransitionTables;
        bool hasTransitionRanges;
        String conditionSQL;
        public String procedureSQL;
        public Statement[] statements = Statement.emptyArray;
        int[] updateColumns;

        // other variables
        public QNameManager.QName name;
        String actionTimingString;
        String eventTimingString;
        int operationPrivilegeType;
        bool forEachRow;
        bool nowait;                                           // block or overwrite if queue full
        int maxRowsQueued;                                    // max size of queue of pending triggers
        public Table table;
        Trigger trigger;
        String triggerClassName;
        public int triggerType;
        Thread thread;

        //protected bool busy;               // firing trigger in progress
        protected EfzDeque pendingQueue;                   // row triggers pending
        protected int rowsQueued;                     // rows in pendingQueue
        protected bool valid = true;               // parsing valid
        protected volatile bool keepGoing = true;

        public TriggerDef() { }

        /**
         *  Constructs a new TriggerDef object to represent an HSQLDB trigger
         *  declared in an SQL CREATE TRIGGER statement.
         *
         *  Changes in 1.7.2 allow the queue size to be specified as 0. A zero
         *  queue size causes the Trigger.fire() code to run in the main thread of
         *  execution (fully inside the enclosing transaction). Otherwise, the code
         *  is run in the Trigger's own thread.
         *  (fredt@users)
         *
         * @param  name The trigger object's HsqlName
         * @param  when the String representation of whether the trigger fires
         *      before or after the triggering event
         * @param  operation the String representation of the triggering operation;
         *      currently insert, update, or delete
         * @param  forEach indicates whether the trigger is fired for each row
         *      (true) or statement (false)
         * @param  table the Table object upon which the indicated operation
         *      fires the trigger
         * @param  triggerClassName the fully qualified named of the class implementing
         *      the org.hsqldb.Trigger (trigger body) interface
         * @param  noWait do not wait for available space on the pending queue; if
         *      the pending queue does not have fewer than nQueueSize queued items,
         *      then overwrite the current tail instead
         * @param  queueSize the length to which the pending queue may grow before
         *      further additions are either blocked or overwrite the tail entry,
         *      as determined by noWait
         */
        public TriggerDef(QNameManager.QName name, String when,
                          String operation, bool forEach, Table table,
                          Table[] transitions, RangeVariable[] rangeVars,
                          Expression condition, String conditionSQL,
                          int[] updateColumns, String triggerClassName,
                          bool noWait, int queueSize)
            : this(name, when, operation, forEach, table, transitions, rangeVars,
                condition, conditionSQL, updateColumns)
        {

            this.triggerClassName = triggerClassName;
            this.nowait = noWait;
            this.maxRowsQueued = queueSize;
            rowsQueued = 0;
            pendingQueue = new EfzDeque();

            Type cl;

            try
            {
                cl = Type.GetType(triggerClassName);
            }
            catch (TypeLoadException )
            {
                valid = false;
                cl = typeof(DefaultTrigger);
            }

            try
            {

                // dynamically instantiate it
                trigger = (Trigger)Activator.CreateInstance(cl);
            }
            catch (Exception )
            {
                valid = false;
                trigger = new DefaultTrigger();
            }
        }

        public TriggerDef(QNameManager.QName name, String when,
                          String operation, bool forEachRow, Table table,
                          Table[] transitions, RangeVariable[] rangeVars,
                          Expression condition, String conditionSQL,
                          int[] updateColumns)
        {

            this.name = name;
            this.actionTimingString = when;
            this.eventTimingString = operation;
            this.forEachRow = forEachRow;
            this.table = table;
            this.transitions = transitions;
            this.rangeVars = rangeVars;
            this.condition = condition == null ? Expression.EXPR_TRUE
                                                        : condition;
            this.updateColumns = updateColumns;
            this.conditionSQL = conditionSQL;
            hasTransitionRanges = transitions[OLD_ROW] != null
                                  || transitions[NEW_ROW] != null;
            hasTransitionTables = transitions[OLD_TABLE] != null
                                  || transitions[NEW_TABLE] != null;

            setUpIndexesAndTypes();
        }

        public bool isValid()
        {
            return valid;
        }

        public virtual int getType()
        {
            return SchemaObjectTypes.TRIGGER;
        }

        public virtual QNameManager.QName getName()
        {
            return name;
        }

        public virtual QNameManager.QName getCatalogName()
        {
            return name.schema.schema;
        }

        public virtual QNameManager.QName getSchemaName()
        {
            return name.schema;
        }

        public virtual Grantee getOwner()
        {
            return name.schema.owner;
        }

        public virtual OrderedHashSet getReferences()
        {
            return new OrderedHashSet();
        }

        public virtual OrderedHashSet getComponents()
        {
            return null;
        }

        public virtual void compile(Session session, SchemaObject parentObject) { }

        /**
         *  Retrieves the SQL character sequence required to (re)create the
         *  trigger, as a StringBuilder
         *
         * @return the SQL character sequence required to (re)create the
         *  trigger
         */
        public virtual String getSQL()
        {

            StringBuilder sb = getSQLMain();

            if (maxRowsQueued != 0)
            {
                sb.Append(Tokens.T_QUEUE).Append(' ');
                sb.Append(maxRowsQueued).Append(' ');

                if (nowait)
                {
                    sb.Append(Tokens.T_NOWAIT).Append(' ');
                }
            }

            sb.Append(Tokens.T_CALL).Append(' ');
            sb.Append(StringConverter.toQuotedString(triggerClassName, '"',
                    false));

            return sb.ToString();
        }

        public StringBuilder getSQLMain()
        {

            StringBuilder sb = new StringBuilder(256);

            sb.Append(Tokens.T_CREATE).Append(' ');
            sb.Append(Tokens.T_TRIGGER).Append(' ');
            sb.Append(name.getSchemaQualifiedStatementName()).Append(' ');
            sb.Append(actionTimingString).Append(' ');
            sb.Append(eventTimingString).Append(' ');

            if (updateColumns != null)
            {
                sb.Append(Tokens.T_OF).Append(' ');

                for (int i = 0; i < updateColumns.Length; i++)
                {
                    if (i != 0)
                    {
                        sb.Append(',');
                    }

                    QNameManager.QName name1 = table.getColumn(updateColumns[i]).getName();

                    sb.Append(name1.statementName);
                }

                sb.Append(' ');
            }

            sb.Append(Tokens.T_ON).Append(' ');
            sb.Append(table.getName().getSchemaQualifiedStatementName()).Append(' ');

            if (hasTransitionRanges || hasTransitionTables)
            {
                sb.Append(Tokens.T_REFERENCING).Append(' ');

                String separator = "";

                if (transitions[OLD_ROW] != null)
                {
                    sb.Append(Tokens.T_OLD).Append(' ').Append(Tokens.T_ROW);
                    sb.Append(' ').Append(Tokens.T_AS).Append(' ');
                    sb.Append(transitions[OLD_ROW].getName().statementName);

                    separator = Tokens.T_COMMA;
                }

                if (transitions[NEW_ROW] != null)
                {
                    sb.Append(separator);
                    sb.Append(Tokens.T_NEW).Append(' ').Append(Tokens.T_ROW);
                    sb.Append(' ').Append(Tokens.T_AS).Append(' ');
                    sb.Append(transitions[NEW_ROW].getName().statementName);

                    separator = Tokens.T_COMMA;
                }

                if (transitions[OLD_TABLE] != null)
                {
                    sb.Append(separator);
                    sb.Append(Tokens.T_OLD).Append(' ').Append(Tokens.T_TABLE);
                    sb.Append(' ').Append(Tokens.T_AS).Append(' ');
                    sb.Append(transitions[OLD_TABLE].getName().statementName);

                    separator = Tokens.T_COMMA;
                }

                if (transitions[NEW_TABLE] != null)
                {
                    sb.Append(separator);
                    sb.Append(Tokens.T_OLD).Append(' ').Append(Tokens.T_TABLE);
                    sb.Append(' ').Append(Tokens.T_AS).Append(' ');
                    sb.Append(transitions[NEW_TABLE].getName().statementName);
                }

                sb.Append(' ');
            }

            if (forEachRow)
            {
                sb.Append(Tokens.T_FOR).Append(' ');
                sb.Append(Tokens.T_EACH).Append(' ');
                sb.Append(Tokens.T_ROW).Append(' ');
            }

            if (condition != Expression.EXPR_TRUE)
            {
                sb.Append(Tokens.T_WHEN).Append(' ');
                sb.Append(Tokens.T_OPENBRACKET).Append(conditionSQL);
                sb.Append(Tokens.T_CLOSEBRACKET).Append(' ');
            }

            return sb;
        }

        public virtual String getClassName()
        {
            return trigger.GetType().FullName;
        }

        public String getActionTimingString()
        {
            return actionTimingString;
        }

        public String getEventTypeString()
        {
            return eventTimingString;
        }

        public bool isForEachRow()
        {
            return forEachRow;
        }

        public String getConditionSQL()
        {
            return conditionSQL;
        }

        public String getProcedureSQL()
        {
            return procedureSQL;
        }

        public int[] getUpdateColumnIndexes()
        {
            return updateColumns;
        }

        public virtual bool hasOldTable()
        {
            return false;
        }

        public virtual bool hasNewTable()
        {
            return false;
        }

        public String getOldTransitionRowName()
        {

            return transitions[OLD_ROW] == null ? null
                                                : transitions[OLD_ROW].getName()
                                                .name;
        }

        public String getNewTransitionRowName()
        {

            return transitions[NEW_ROW] == null ? null
                                                : transitions[NEW_ROW].getName()
                                                .name;
        }

        public String getOldTransitionTableName()
        {

            return transitions[OLD_TABLE] == null ? null
                                                  : transitions[OLD_TABLE]
                                                  .getName().name;
        }

        public String getNewTransitionTableName()
        {

            return transitions[NEW_TABLE] == null ? null
                                                  : transitions[NEW_TABLE]
                                                  .getName().name;
        }

        /**
         *  Given the SQL creating the trigger, set up the index to the
         *  HsqlArrayList[] and the associated GRANT type
         */
        void setUpIndexesAndTypes()
        {

            triggerType = 0;

            if (eventTimingString.Equals(Tokens.T_INSERT))
            {
                triggerType = TriggerConstants.INSERT_AFTER;
                operationPrivilegeType = GrantConstants.INSERT;
            }
            else if (eventTimingString.Equals(Tokens.T_DELETE))
            {
                operationPrivilegeType = GrantConstants.DELETE;
                triggerType = TriggerConstants.DELETE_AFTER;
            }
            else if (eventTimingString.Equals(Tokens.T_UPDATE))
            {
                operationPrivilegeType = GrantConstants.UPDATE;
                triggerType = TriggerConstants.UPDATE_AFTER;
            }
            else
            {
                throw Error.runtimeError(ErrorCode.U_S0500, "TriggerDef");
            }

            if (actionTimingString.Equals(Tokens.T_BEFORE))
            {
                triggerType += NUM_TRIGGER_OPS;    // number of operations
            }

           // triggerType = triggerType;

            if (forEachRow)
            {
                triggerType += NUM_TRIGGER_OPS;
            }
        }

        public int getPrivilegeType()
        {
            return operationPrivilegeType;
        }

        /**
         *  run method declaration <P>
         *
         *  the trigger JSP is run in its own thread here. Its job is simply to
         *  wait until it is told by the main thread that it should fire the
         *  trigger.
         */
        public void run()
        {

            while (keepGoing)
            {
                TriggerData triggerData = popPair();

                if (triggerData != null)
                {
                    if (triggerData.username != null)
                    {
                        trigger.fire(this.triggerType, name.name,
                                     table.getName().name, triggerData.oldRow,
                                     triggerData.newRow);
                    }
                }
            }
        }

        /**
         * start the thread if this is threaded
         */
        public void start()
        {
            lock (this)
            {
                if (maxRowsQueued != 0)
                {
                    thread = new Thread(run);

                    thread.Start();
                }
            }
        }

        /**
         * signal the thread to stop
         */
        public void terminate()
        {
            lock (this)
            {
                keepGoing = false;

                Monitor.Pulse(this);
            }
        }

        /**
         *  pop2 method declaration <P>
         *
         *  The consumer (trigger) thread waits for an event to be queued <P>
         *
         *  <B>Note: </B> This push/pop pairing assumes a single producer thread
         *  and a single consumer thread _only_.
         *
         * @return  Description of the Return Value
         */
        private TriggerData popPair()
        {
            lock (this)
            {
                if (rowsQueued == 0)
                {
                    try
                    {
                        Monitor.Wait(this);    // this releases the lock monitor
                    }
                    catch (ThreadInterruptedException )
                    {

                        /* ignore and resume */
                    }
                }

                rowsQueued--;

                Monitor.Pulse(this);    // notify push's wait

                if (pendingQueue.size() == 0)
                {
                    return null;
                }
                else
                {
                    return (TriggerData)pendingQueue.removeFirst();
                }
            }
        }

        /**
         *  The main thread tells the trigger thread to fire by this call.
         *  If this Trigger is not threaded then the fire method is caled
         *  immediately and executed by the main thread. Otherwise, the row
         *  data objects are added to the queue to be used by the Trigger thread.
         *
         * @param  row1
         * @param  row2
         */
        public virtual void pushPair(Session session, Object[] row1, Object[] row2)
        {
            lock (this)
            {
                if (maxRowsQueued == 0)
                {
                    trigger.fire(triggerType, name.name, table.getName().name, row1,
                                 row2);

                    return;
                }

                if (rowsQueued >= maxRowsQueued)
                {
                    if (nowait)
                    {
                        pendingQueue.removeLast();    // overwrite last
                    }
                    else
                    {
                        try
                        {
                            Monitor.Wait(this);
                        }
                        catch (ThreadInterruptedException )
                        {

                            /* ignore and resume */
                        }

                        rowsQueued++;
                    }
                }
                else
                {
                    rowsQueued++;
                }

                pendingQueue.add(new TriggerData(session, row1, row2));
                Monitor.Pulse(this);    // notify pop's wait
            }
        }

        public bool isBusy()
        {
            return rowsQueued != 0;
        }

        public Table getTable()
        {
            return table;
        }

        public String getActionOrientationString()
        {
            return forEachRow ? Tokens.T_ROW
                              : Tokens.T_STATEMENT;
        }

        /**
         * Class to store the data used to fire a trigger. The username attribute
         * is not used but it allows developers to change the signature of the
         * fire method of the Trigger class and pass the user name to the Trigger.
         */
        public class TriggerData
        {

            public Object[] oldRow;
            public Object[] newRow;
            public String username;

            public TriggerData(Session session, Object[] oldRow, Object[] newRow)
            {

                this.oldRow = oldRow;
                this.newRow = newRow;
                this.username = session.getUsername();
            }
        }

        public class DefaultTrigger : Trigger
        {

            public void fire(int i, String name, String table, Object[] row1,
                             Object[] row2)
            {
                throw new Exception("Missing Trigger class!");
            }
        }

    }

}
